/*
 * Copyright (c) 2013, Inmite s.r.o. (www.inmite.eu).
 *
 * All rights reserved. This source code can be used only for purposes specified
 * by the given license contract signed by the rightful deputy of Inmite s.r.o.
 * This source code can be used only by the owner of the license.
 *
 * Any disputes arising in respect of this agreement (license) shall be brought
 * before the Municipal Court of Prague.
 */

package eu.inmite.harmony.lib.validations.form;

import eu.inmite.harmony.lib.validations.exception.FormsValidationException;
import eu.inmite.harmony.lib.validations.exception.NoFieldAdapterException;
import eu.inmite.harmony.lib.validations.form.annotations.Condition;
import eu.inmite.harmony.lib.validations.form.annotations.Custom;
import eu.inmite.harmony.lib.validations.form.annotations.Joined;
import eu.inmite.harmony.lib.validations.form.annotations.ValidatorFor;
import eu.inmite.harmony.lib.validations.form.iface.ICondition;
import eu.inmite.harmony.lib.validations.form.iface.IFieldAdapter;
import eu.inmite.harmony.lib.validations.form.iface.IValidationCallback;
import eu.inmite.harmony.lib.validations.form.iface.IValidator;
import eu.inmite.harmony.lib.validations.form.validators.ValidatorFactory;

import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.ability.fraction.Fraction;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentTreeObserver;
import ohos.app.Context;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * Validate forms. Use either {@link #validate(Context, IValidationCallback)} methods
 * to validate all views at once or start live validation by calling
 * {@link #startLiveValidation(Context, IValidationCallback)}.
 * </p>
 * <p>
 * Views inherited from {@link ohos.agp.components.Text} are automatically recognized and can be validated,
 * for other views you need to provide adapter and
 * register it by calling {@link #registerViewAdapter(Class, Class)}.
 * </p>
 * <p>
 * To validate multiple views at once by one validator, use {@link Joined} annotation. <br/>
 * You can also add condition to view and the validation will proceed only of the given condition is met,
 * see {@link Condition}.
 * </p>
 * <p>
 * For all available validations see package {@link eu.inmite.harmony.lib.validations.form.annotations}.
 * To add custom validation you can use {@link Custom} annotation and provide own {@link IValidator}.
 * </p>
 *
 * @author Tomas Vondracek
 */
public class FormValidator {
    private static Map<Object, ViewGlobalFocusChangeListener> sLiveValidations;

    private FormValidator() {
    }

    /**
     * Register custom validator. This is only usable if you want to use custom validation annotations.
     * Otherwise you can use {@link Custom} annotation.
     *
     * @param validator validator class that is annotated with {@link ValidatorFor} annotation.
     * @throws IllegalArgumentException if validator is null
     */
    public static void registerValidator(Class<? extends IValidator<?>> validator) {
        if (validator == null) {
            throw new IllegalArgumentException("validator cannot be null");
        }

        ValidatorFactory.registerValidatorClasses(validator);
    }

    /**
     * Register adapter that can be used to get value from view.
     *
     * @param viewType type of view adapter is determined to get values from
     * @param adapterClass class of adapter to register
     * @throws IllegalArgumentException if adapterClass is null or viewType is null
     * @throws FormsValidationException when there is a problem when accessing adapter class
     */
    public static void registerViewAdapter(Class<? extends Component> viewType,
        Class<? extends IFieldAdapter<? extends Component, ?>> adapterClass) {
        if (viewType == null || adapterClass == null) {
            throw new IllegalArgumentException("arguments must not be null");
        }

        try {
            FieldAdapterFactory.registerAdapter(viewType, adapterClass);
        } catch (IllegalAccessException e) {
            throw new FormsValidationException(e);
        } catch (InstantiationException e) {
            throw new FormsValidationException(e);
        }
    }

    /**
     * remove all adapters that have been registered with {@link #registerValidator(Class)}
     */
    public static void clearViewAdapters() {
        FieldAdapterFactory.clear();
    }

    /**
     * clear()
     */
    public static void clear() {
        ValidatorFactory.clearCachedValidators();
    }

    /**
     * Clear fields cache for all targets. In general this isn't necessary since cache is freed automatically.
     *
     * @return boolean
     */
    public static boolean clearCaches() {
        return FieldFinder.clearCache();
    }

    /**
     * Start live validation - whenever focus changes from view with validations upon itself, validators will run. <br/>
     * Don't forget to call {@link #stopLiveValidation(Object)} once you are done.
     *
     * @param fragment fragment with views to validate, there can be only one continuous validation per target object
     *                 (fragment)
     * @param callback callback invoked whenever there is some validation fail
     */
    public static void startLiveValidation(final Fraction fragment, final IValidationCallback callback) {
        startLiveValidation(fragment, fragment.getComponent(), callback);
    }

    /**
     * Start live validation - whenever focus changes from view with validations upon itself, validators will run.<br/>
     * Don't forget to call {@link #stopLiveValidation(Object)} once you are done.
     *
     * @param target target with views to validate, there can be only one continuous validation per target
     * @param formContainer view that contains our form (views to validate)
     * @param callback callback invoked whenever there is some validation fail, can be null
     * @throws IllegalArgumentException if formContainer or target is null
     */
    public static void startLiveValidation(final Object target, final Component formContainer,
        final IValidationCallback callback) {
        if (formContainer == null) {
            throw new IllegalArgumentException("form container view cannot be null");
        }
        if (target == null) {
            throw new IllegalArgumentException("target cannot be null");
        }

        if (sLiveValidations == null) {
            sLiveValidations = new HashMap<>();
        } else if (sLiveValidations.containsKey(target)) {
            // validation is already running
            return;
        }

        final Map<Component, FieldInfo> infoMap = FieldFinder.getFieldsForTarget(target);
        final ViewGlobalFocusChangeListener listener = new ViewGlobalFocusChangeListener(infoMap, formContainer, target,
            callback);
        final ComponentTreeObserver observer = formContainer.getComponentTreeObserver();
        observer.addGlobalFocusUpdatedListener(listener);
        sLiveValidations.put(target, listener);
    }

    /**
     * isLiveValidationRunning
     *
     * @param target target
     * @return boolean
     */
    public static boolean isLiveValidationRunning(final Object target) {
        return sLiveValidations != null && sLiveValidations.containsKey(target);
    }

    /**
     * stop previously started live validation by {@link #startLiveValidation(Object, ohos.agp.components.Component,
     * IValidationCallback)}
     *
     * @param target live validation is recognized by target object
     * @return true if there was live validation to stop
     */
    public static boolean stopLiveValidation(final Object target) {
        if (sLiveValidations == null || !sLiveValidations.containsKey(target)) {
            return false;
        }
        final ViewGlobalFocusChangeListener removed = sLiveValidations.remove(target);
        final ComponentTreeObserver treeObserver = removed.formContainer.getComponentTreeObserver();
        if (treeObserver != null) {
            treeObserver.removeGlobalFocusUpdatedListener(removed);
            return true;
        }

        return false;
    }

    /**
     * Perform validation over the views of the activity.
     *
     * @param activity activity with views to validate
     * @param callback callback the will receive result of validation, results are ordered by order param in annotation.
     * @return whether the validation succeeded
     */
    public static boolean validate(AbilitySlice activity, IValidationCallback callback) {
        return validate(activity, activity, callback);
    }

    /**
     * Perform validation over the views of given fragment
     *
     * @param fragment fragment with views to validate
     * @param callback callback the will receive result of validation, results are ordered by order param in annotation.
     * @return whether the validation succeeded
     */
    public static boolean validate(Fraction fragment, IValidationCallback callback) {
        return validate(fragment.getFractionAbility(), fragment, callback);
    }

    /**
     * Perform validation over all fields on the target object. <br/>
     * Please note that if you have used joined validations (see {@link Joined} over several fields at the same time,
     * target needs to be of type {@link ohos.aafwk.ability.Ability}, {@link ohos.aafwk.ability.fraction.
     * FractionAbility} or {@link ohos.agp.components.ComponentContainer}. Exception will be thrown otherwise.
     *
     * @param context context
     * @param target target
     * @param callback callback
     * @return boolean
     * @throws FormsValidationException if there is problem in validation process
     * @throws IllegalArgumentException if context or target is null
     */
    public synchronized static boolean validate(Context context, Object target, IValidationCallback callback)
        throws FormsValidationException {
        if (context == null) {
            throw new IllegalArgumentException("context cannot ben null");
        }
        if (target == null) {
            throw new IllegalArgumentException("target cannot be null");
        }

        final List<ValidationFail> failedValidations = new ArrayList<>();
        final List<Component> passedValidations = new ArrayList<>();
        boolean result = true;

        final Map<Component, FieldInfo> infoMap = FieldFinder.getFieldsForTarget(target);
        for (Map.Entry<Component, FieldInfo> entry : infoMap.entrySet()) {
            final FieldInfo fieldInfo = entry.getValue();
            final Component view = entry.getKey();

            if (view.getVisibility() == Component.HIDE || view.getVisibility() == Component.INVISIBLE) {
                // don't run validation on views that are not visible
                continue;
            }

            ValidationFail fieldResult = performFieldValidations(context, fieldInfo, view);
            if (fieldResult != null) {
                failedValidations.add(fieldResult);
                result = false;
            } else {
                passedValidations.add(view);
            }
        }

        if (callback != null) {
            Collections.sort(failedValidations, new Comparator<ValidationFail>() {
                @Override
                public int compare(ValidationFail lhs, ValidationFail rhs) {
                    return lhs.order < rhs.order ? -1 : (lhs.order == rhs.order ? 0 : 1);
                }
            });
            callback.validationComplete(result, Collections.unmodifiableList(failedValidations),
                Collections.unmodifiableList(passedValidations));
        }
        return result;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Single view validations
    ///////////////////////////////////////////////////////////////////////////

    /**
     * validateSingleView
     *
     * @param activity ability
     * @param targetView target
     * @param callback callback
     * @return boolean
     */
    public static boolean validateSingleView(AbilitySlice activity, Component targetView,
        IValidationCallback callback) {
        return validateSingleView(activity, activity.getWindow().getCurrentComponentFocus().get(),
            targetView, callback);
    }

    /**
     * validateSingleView
     *
     * @param fragment fraction ability
     * @param targetView target
     * @param callback callback
     * @return boolean
     */
    public static boolean validateSingleView(Fraction fragment, Component targetView, IValidationCallback callback) {
        return validateSingleView(fragment, fragment.getComponent(), targetView, callback);
    }

    /**
     * validateSingleView
     *
     * @param target target
     * @param formContainer container
     * @param targetView component
     * @param callback callback
     * @return boolean
     */
    public static boolean validateSingleView(Object target, Component formContainer, Component targetView,
        IValidationCallback callback) {
        return validateSingleView(target, formContainer, targetView, FieldFinder.getFieldsForTarget(target), callback);
    }

    private static boolean validateSingleView(Object target, Component formContainer, Component targetView,
        Map<Component, FieldInfo> infoMap, IValidationCallback callback) {
        boolean overallResult = false;
        final FieldInfo info = infoMap.get(targetView);
        if (info != null) {
            final ValidationFail validationFail = performFieldValidations(formContainer.getContext(), info, targetView);
            overallResult = validationFail == null;

            if (validationFail != null && callback != null) {
                // we have a failed validation
                callback.validationComplete(false, Collections.singletonList(validationFail),
                    Collections.<Component>emptyList());
            } else if (callback != null) {
                final List<ValidationFail> noFailedValidations = Collections.emptyList();
                overallResult = validate(formContainer.getContext(), target, null);
                callback.validationComplete(overallResult, noFailedValidations, Collections.singletonList(targetView));
            }
        }
        return overallResult;
    }

    /**
     * perform all validations on single field
     * @param context context
     * @param fieldInfo filed info
     * @param view component
     * @return boolean true/false
     */
    private static ValidationFail performFieldValidations(Context context, FieldInfo fieldInfo, Component view) {
        // first, we need to check if condition is not applied for all validations on field
        if (fieldInfo.condition != null && fieldInfo.condition.validationAnnotation().equals(Condition.class)) {
            // condition is applied to all validations on field
            boolean evaluation = evaluateCondition(view, fieldInfo.condition);
            if (!evaluation) {
                // go to next field
                return null;
            }
        }

        // field validations
        for (ValidationInfo valInfo : fieldInfo.validationInfoList) {
            final Annotation annotation = valInfo.annotation;
            if (fieldInfo.condition != null && fieldInfo.condition.validationAnnotation()
                .equals(annotation.annotationType())) {
                boolean evaluation = evaluateCondition(view, fieldInfo.condition);

                if (!evaluation) {
                    // continue to next annotation
                    continue;
                }
            }
            final IFieldAdapter adapter = FieldAdapterFactory.getAdapterForField(view, annotation);
            if (adapter == null) {
                throw new NoFieldAdapterException(view, annotation);
            }

            final Object value = adapter.getFieldValue(annotation, view);
            final boolean isValid = valInfo.validator.validate(annotation, value);

            if (!isValid) {
                final String message = valInfo.validator.getMessage(context, annotation, value);
                final int order = valInfo.validator.getOrder(annotation);

                // no more validations on this field
                return new ValidationFail(view, message, order);
            }
        }
        return null;
    }

    private static boolean evaluateCondition(Component targetView, Condition conditionAnnotation) {
        final int viewId = conditionAnnotation.viewId();
        final Component conditionView = ((Component) targetView.getComponentParent()).findComponentById(viewId);

        final Class<? extends ICondition> clazz = conditionAnnotation.value();
        final IFieldAdapter adapter = FieldAdapterFactory.getAdapterForField(conditionView);
        final Object value = adapter.getFieldValue(null, conditionView);
        try {
            return clazz.newInstance().evaluate(value);
        } catch (InstantiationException e) {
            throw new FormsValidationException(e);
        } catch (IllegalAccessException e) {
            throw new FormsValidationException(e);
        }
    }

    /**
     * ValidationInfo
     */
    static class ValidationInfo {
        final int order;
        private final Annotation annotation;
        private final IValidator validator;

        ValidationInfo(Annotation annotation, IValidator validator) {
            this.annotation = annotation;
            this.validator = validator;
            order = validator.getOrder(annotation);
        }
    }

    /**
     * FieldInfo
     */
    static class FieldInfo {
        private final Condition condition;

        private final List<ValidationInfo> validationInfoList;

        FieldInfo(Condition condition, List<ValidationInfo> validationInfoList) {
            this.condition = condition;
            this.validationInfoList = validationInfoList;
        }
    }

    /**
     * Holds information about failed validation on concrete view
     */
    public static final class ValidationFail {
        /**
         * validation component
         */
        public final Component view;

        /**
         * message
         */
        public final String message;

        final int order;

        private ValidationFail(Component view, String message, int order) {
            this.view = view;
            this.message = message;
            this.order = order;
        }
    }

    /**
     * ViewGlobalFocusChangeListener
     */
    static class ViewGlobalFocusChangeListener implements ComponentTreeObserver.GlobalFocusUpdatedListener {
        private final Map<Component, FieldInfo> infoMap;

        private final Component formContainer;

        private final Object target;

        private final IValidationCallback callback;

        private Component currentlyFocusedView;

        /**
         * Constructor
         *
         * @param infoMap info map
         * @param formContainer container
         * @param target target
         * @param callback callback
         */
        public ViewGlobalFocusChangeListener(Map<Component, FieldInfo> infoMap, Component formContainer, Object target,
            IValidationCallback callback) {
            this.infoMap = infoMap;
            this.formContainer = formContainer;
            this.target = target;
            this.callback = callback;
            currentlyFocusedView = formContainer.findFocus();
        }

        @Override
        public void onGlobalFocusUpdated(Component oldFocus, Component newFocus) {
            // dunno why, but oldFocus is absolutely wrong
            if (currentlyFocusedView != null && currentlyFocusedView != newFocus) {
                validateSingleView(target, formContainer, currentlyFocusedView, infoMap, callback);
            }
            currentlyFocusedView = newFocus;
        }
    }
}
